nestjs exception filter 설계하여 Known exception들을 500 에러가 아닌 statusCode로 응답 보내기
다음과 같은 PrismaClientKnownRequestError 에러가 발생했을 때 500에러가 아니라 AlreadyExists 같은 4XX 시리즈 에러로 보내게끔 하기 위하여 매번 Exception Class를 만드는 것은 너무 번거롭다. 따라서, NestJS Exception Filter를 만들어 적어도 Prisma가 일으키는 예외들을 처리할 수 있도록 하자.
{
"body": {
"title": "난이도",
"iconSvg": "<svg width=\"100\" height=\"100\" xmlns=\"http://www.w3.org/2000/svg\"><circle cx=\"50\" cy=\"50\" r=\"40\" fill=\"red\" /></svg>",
"isRequired": true,
"highlight": true
},
"query": {},
"url": "/coach-form",
"ip": "::1",
"exception": {
"name": "PrismaClientKnownRequestError",
"code": "P2002",
"clientVersion": "5.0.0",
"meta": {
"target": "coachdetailkeymodels_title_key"
}
},
"message": "\nInvalid `.create()` invocation in\n/Users/choiwheatley/workspace/racketime-api/src/coach/coach-form.repository.ts:29:8\n\n 26 ).then((v) => v.order);\n 27 \n 28 return await this.prisma.coachDetailKey\n→ 29 .create(\nUnique constraint failed on the constraint: `coachdetailkeymodels_title_key`",
"error": "에러가 발생했습니다."
}
ChatGPT o4-mini-high 응답
NestJS 전역 필터 하나로 Prisma 에러를 4XX 응답으로 매핑하는 예시입니다.
// prisma-exception.filter.ts
import {
ExceptionFilter,
Catch,
ArgumentsHost,
HttpException,
ConflictException,
BadRequestException,
NotFoundException,
InternalServerErrorException,
} from '@nestjs/common';
import { Prisma } from '@prisma/client';
@Catch(
Prisma.PrismaClientKnownRequestError,
Prisma.PrismaClientValidationError,
Prisma.PrismaClientUnknownRequestError,
)
export class PrismaExceptionFilter implements ExceptionFilter {
catch(exception: any, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse();
const request = ctx.getRequest();
let httpEx: HttpException;
// KnownRequestError: 코드별로 매핑
if (exception instanceof Prisma.PrismaClientKnownRequestError) {
switch (exception.code) {
case 'P2002': {
// unique constraint failed
// meta.target에 중복된 컬럼 이름이 들어있음
const target = Array.isArray(exception.meta.target)
? exception.meta.target.join(', ')
: exception.meta.target;
httpEx = new ConflictException(`${target} 은(는) 이미 존재합니다.`);
break;
}
case 'P2025':
// record not found for update/delete
httpEx = new NotFoundException('요청한 리소스를 찾을 수 없습니다.');
break;
case 'P2003':
case 'P2004':
case 'P2005':
// foreign key, value too long 등
httpEx = new BadRequestException(exception.message);
break;
default:
httpEx = new InternalServerErrorException('알 수 없는 데이터베이스 오류입니다.');
}
}
// ValidationError: 스키마 검증 실패
else if (exception instanceof Prisma.PrismaClientValidationError) {
httpEx = new BadRequestException(exception.message);
}
// UnknownRequestError: 커넥션 등
else if (exception instanceof Prisma.PrismaClientUnknownRequestError) {
httpEx = new InternalServerErrorException('데이터베이스 요청 중 오류가 발생했습니다.');
}
// 그 외
else {
httpEx = new InternalServerErrorException('서버 오류입니다.');
}
response
.status(httpEx.getStatus())
.json({
statusCode: httpEx.getStatus(),
timestamp: new Date().toISOString(),
path: request.url,
message: httpEx.message,
});
}
}
그리고 이 필터를 main.ts
에 전역 등록하면, Prisma 에러가 발생할 때마다 500이 아닌 적절한 4XX/5XX로 자동 변환됩니다.
// main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { PrismaExceptionFilter } from './filters/prisma-exception.filter';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// 전역 필터 등록
app.useGlobalFilters(new PrismaExceptionFilter());
await app.listen(3000);
}
bootstrap();
코드 설명
-
@Catch(...)
Prisma가 던지는 주요 예외 클래스들을 한 번에 잡아냅니다. -
PrismaClientKnownRequestError.code
P2002
: unique constraint 위반 →409 Conflict
P2025
: 대상 레코드 없음 →404 Not Found
- 그 외 foreign key 등 →
400 Bad Request
-
PrismaClientValidationError
스키마/쿼리 검증 실패 →400 Bad Request
-
PrismaClientUnknownRequestError
예기치 않은 내부 요청 오류 →500 Internal Server Error
-
응답 포맷
{ "statusCode": 409, "timestamp": "2025-04-30T12:34:56.789Z", "path": "/coach-form", "message": "coachdetailkeymodels.title already exists." }
이제 매번 개별 Exception 클래스를 만들지 않고도 Prisma 예외를 깔끔하게 4XX 에러로 변환할 수 있습니다.
Q. I need something MORE
아래는 “더욱 강력하게” 동작하는 Prisma 예외 필터 예시입니다.
- **DI(의존성 주입)**로
Logger
와ConfigService
를 받아와서- 에러 발생 시 로깅
- 필요하면 런타임에 에러 ↔ HTTP 상태·메시지 매핑을 변경 가능
- NestJS의 글로벌
APP_FILTER
프로바이더로 등록하여new
생성 없이도 모든 컨텍스트(REST, GraphQL)에 적용 - 매핑 테이블을 이용해 나중에 코드 추가·수정이 편리
// src/filters/prisma-exception.filter.ts
import {
Injectable,
Logger,
HttpException,
HttpStatus,
ArgumentsHost,
Catch,
ExceptionFilter,
} from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { Prisma } from '@prisma/client';
interface PrismaErrorMapEntry {
status: HttpStatus;
message: (exc: Prisma.PrismaClientKnownRequestError) => string;
}
@Injectable()
@Catch(
Prisma.PrismaClientKnownRequestError,
Prisma.PrismaClientValidationError,
Prisma.PrismaClientUnknownRequestError,
)
export class PrismaExceptionFilter implements ExceptionFilter {
private readonly logger = new Logger(PrismaExceptionFilter.name);
// 기본 매핑 테이블
private readonly defaultMap: Record<string, PrismaErrorMapEntry> = {
P2002: {
status: HttpStatus.CONFLICT,
message: exc => {
const target = Array.isArray(exc.meta.target)
? exc.meta.target.join(', ')
: exc.meta.target;
return `${target} 은(는) 이미 존재합니다.`;
},
},
P2025: {
status: HttpStatus.NOT_FOUND,
message: () => `요청한 리소스를 찾을 수 없습니다.`,
},
P2003: {
status: HttpStatus.BAD_REQUEST,
message: exc => exc.message,
},
// 필요시 여기에 추가…
};
private readonly errorMap: Record<string, PrismaErrorMapEntry>;
constructor(private readonly config: ConfigService) {
// config 서비스에서 오버라이드 가능 (예: 환경변수로 맵핑 정의)
this.errorMap = this.config.get<Record<string, PrismaErrorMapEntry>>(
'PRISMA_ERROR_MAP',
this.defaultMap,
);
}
catch(exception: unknown, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse();
const request = ctx.getRequest();
let status: number;
let message: string;
// KnownRequestError
if (exception instanceof Prisma.PrismaClientKnownRequestError) {
const entry = this.errorMap[exception.code];
if (entry) {
status = entry.status;
message = entry.message(exception);
} else {
status = HttpStatus.INTERNAL_SERVER_ERROR;
message = '알 수 없는 데이터베이스 오류입니다.';
}
this.logger.warn(
`PrismaClientKnownRequestError(code=${exception.code}): ${exception.message}`,
);
}
// ValidationError
else if (exception instanceof Prisma.PrismaClientValidationError) {
status = HttpStatus.BAD_REQUEST;
message = exception.message;
this.logger.warn(`PrismaClientValidationError: ${message}`);
}
// UnknownRequestError
else if (exception instanceof Prisma.PrismaClientUnknownRequestError) {
status = HttpStatus.INTERNAL_SERVER_ERROR;
message = '데이터베이스 요청 중 오류가 발생했습니다.';
this.logger.error(`PrismaClientUnknownRequestError: ${exception}`);
}
// 그 외
else {
status = HttpStatus.INTERNAL_SERVER_ERROR;
message = '서버 오류입니다.';
this.logger.error(`Non-Prisma exception passed to filter: ${exception}`);
}
response.status(status).json({
statusCode: status,
timestamp: new Date().toISOString(),
path: request.url,
message,
});
}
}
// src/app.module.ts
import { Module } from '@nestjs/common';
import { APP_FILTER } from '@nestjs/core';
import { PrismaExceptionFilter } from './filters/prisma-exception.filter';
import { ConfigModule } from '@nestjs/config';
@Module({
imports: [ConfigModule.forRoot(/* env 설정 */)],
providers: [
{
provide: APP_FILTER,
useClass: PrismaExceptionFilter,
},
],
})
export class AppModule {}
주요 개선 포인트
- 의존성 주입
Logger
+ConfigService
활용 → 런타임에 매핑 수정 & 통일된 로깅
- 매핑 테이블
defaultMap
에 에러코드별status
·message
정의- 환경변수(
PRISMA_ERROR_MAP
)로 쉽게 오버라이드
- 글로벌 필터 등록
APP_FILTER
토큰 사용 → 모든 컨트롤러/GraphQL에 자동 적용
- 확장성
- 새로운 Prisma 에러코드 추가 시
defaultMap
에 한 줄만 추가
- 새로운 Prisma 에러코드 추가 시
이제 다음과 같은 장점이 있습니다.
- 추가 예외 클래스 없이도 다양한 Prisma 에러 대응
- 운영환경에서 메시지·HTTP 상태만 환경변수로 조정 가능
- 일관된 로깅 및 에러 포맷
- GraphQL 프로젝트에도 그대로 재사용
필요하다면 여기에 i18n, 추적ID(correlation-id) 주입, Sentry 연동 등을 더해도 좋습니다!